﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;

namespace FerrSimplicity
{
    /// <summary>
    /// Represents a single frame of animation, used for the ComActor2D class
    /// </summary>
    public struct AnimFrame
    {
        /// <summary>
        /// The tile ID for this frame of animation, (IDs start at zero, which is the 
        /// top left corner of the image, and continue to the right )
        /// </summary>
        public int   mTile;
        /// <summary>
        /// Measured in seconds, how many seconds should this frame stay up before 
        /// moving to the next?
        /// </summary>
        public float mDuration;
        /// <summary>
        /// Just a basic constructor, to make it easier to create arrays of animations
        /// </summary>
        /// <param name="aTileID"></param>
        /// <param name="aFrameDuration"></param>
        public AnimFrame(int aTileID, float aFrameDuration)
        {
            mTile     = aTileID;
            mDuration = aFrameDuration;
        }
    }

    /// <summary>
    /// Used by the ComActor2D class. An entire animation, with a few 
    /// attributes to describe how it should animate.
    /// </summary>
    public class Anim
    {
        /// <summary>
        /// List of animation frames
        /// </summary>
        public AnimFrame[] mAnim;
        /// <summary>
        /// The entire duration of the animation, to make our lives easier
        /// </summary>
        public float       mDuration;
        /// <summary>
        /// What animation comes after this one?
        /// </summary>
        public string      mNextAnim;
        /// <summary>
        /// Does this animation loop when it gets to the end?
        /// </summary>
        public bool        mLoop;
    }

    /// <summary>
    /// A game object component, used to draw animated 2D sprites. Add it to 
    /// your game object, and it draws using the objects location information!
    /// </summary>
    public class ComActor2D : ComSprite2D
    {
        #region Fields
        protected Dictionary<string, Anim> mAnimations;

        protected string mCurrentAnim = "";
        protected float  mAnimTime    = 0;

        protected int    mGridWidth;
        protected int    mGridHeight;

        /// <summary>
        /// A delegate for use with the OnAnimFinish event
        /// </summary>
        /// <param name="aSource">The ComActor2D that is raising the event</param>
        /// <param name="aAnimName">The name of the animation that just finished</param>
        public delegate void  AnimEventDelegate(ComActor2D aSource, string aAnimName);
        /// <summary>
        /// When the current animation finishes, this event is raised, with this 
        /// component, and the name of the animation that just finished.
        /// </summary>
        public          event AnimEventDelegate OnAnimFinish;
        #endregion

        #region Properties
        /// <summary>
        /// A multiplier to change the speed of the animation. Default is 1, and 
        /// for example, 2 would double the animation speed, 0.5 would half it.
        /// </summary>
        public float  AnimSpeed   { get; set; }
        /// <summary>
        /// Returns the current animation
        /// </summary>
        public string CurrentAnim { get { return mCurrentAnim; } }
        /// <summary>
        /// The width of each cell of animation on the sprite sheet
        /// </summary>
        public int    CellSizeX   { get; set; }
        /// <summary>
        /// The height of each cell of animation on the sprite sheet
        /// </summary>
        public int    CellSizeY   { get; set; }
        #endregion

        #region Constructors
        /// <summary>
        /// A default parameterless constructor for use with cloning
        /// </summary>
        public ComActor2D()
            : this("", 0, 0) { }
        /// <summary>
        /// Creates a basic animated sprite component
        /// </summary>
        /// <param name="aSpriteSheet">The name of the image used for the spritesheet</param>
        /// <param name="aCellSizeX">The width of each cell of animation</param>
        /// <param name="aCellSizeY">The height of each cell of animation</param>
        public ComActor2D(string aSpriteSheet, int aCellSizeX, int aCellSizeY)
            : base(aSpriteSheet) 
        { 
            CellSizeX = aCellSizeX; 
            CellSizeY = aCellSizeY;

            mAnimations = new Dictionary<string, Anim>();

            // default value for the AnimSpeed
            AnimSpeed = 1;

            // set the source size specifically so the origin can be properly determined
            Source = new Rectangle(0, 0, aCellSizeX, aCellSizeY);
        }
        #endregion

        #region Methods
        /// <summary>
        /// Adds an animation to the actor's list of animations. If this is the first
        /// animation added, or the name tolower is "default", it is automatically set 
        /// as the active animation. Will throw exceptions if name already exists.
        /// </summary>
        /// <param name="aName">Name by which this animation is refered to. A name of 'default' will start this animation automatically</param>
        /// <param name="aAnim">An array of frame information for the animation</param>
        public void AddAnimation(string aName, AnimFrame[] aAnim)             { AddAnimation(aName, aAnim, true,  ""); }
        /// <summary>
        /// Adds an animation to the actor's list of animations. If this is the first
        /// animation added, or the name tolower is "default", it is automatically set 
        /// as the active animation. Will throw exceptions if name already exists.
        /// </summary>
        /// <param name="aName">Name by which this animation is refered to. A name of 'default' will start this animation automatically</param>
        /// <param name="aAnim">An array of frame information for the animation</param>
        /// <param name="aLoop">Should the animation play again when finished?</param>
        public void AddAnimation(string aName, AnimFrame[] aAnim, bool aLoop) { AddAnimation(aName, aAnim, aLoop, ""); }
        /// <summary>
        /// Adds an animation to the actor's list of animations. If this is the first
        /// animation added, or the name tolower is "default", it is automatically set 
        /// as the active animation. Will throw exceptions if name already exists.
        /// </summary>
        /// <param name="aName">Name by which this animation is refered to. A name of 'default' will start this animation automatically</param>
        /// <param name="aAnim">An array of frame information for the animation</param>
        /// <param name="aLoop">Should the animation play again when finished?</param>
        /// <param name="aNextAnim">What animation should the actor go to when finished with this one?</param>
        public void AddAnimation(string aName, AnimFrame[] aAnim, bool aLoop, string aNextAnim)
        {
            // check if it already exists
            if (mAnimations.ContainsKey(aName))
                throw new ArgumentException("An animation with that name already exists!");

            Anim anim = new Anim();
            anim.mAnim     = aAnim;
            anim.mNextAnim = aNextAnim;
            anim.mLoop     = aLoop;
            for (int i = 0; i < aAnim.Length; i++)
                anim.mDuration += aAnim[i].mDuration;
            mAnimations.Add(aName, anim);

            // set the first anim, or 'default' as the default anim
            if (mCurrentAnim == "" || aName.ToLower() == "default")
                SetAnim( aName );
        }
        /// <summary>
        /// Sets the active animation, throws exceptions if the name
        /// is wrong.
        /// </summary>
        /// <param name="aAnimName">Name of the animation to start.</param>
        public void SetAnim     (string aAnimName)
        {
            if (mAnimations.ContainsKey(aAnimName) || aAnimName == "")
            {
                mCurrentAnim = aAnimName;
                mAnimTime    = 0;
            }
            else
            {
                throw new ArgumentException("No animation with that name exists!");
            }
        }
        /// <summary>
        /// Checks to see if an animation with the given name exists in the animation list.
        /// </summary>
        /// <param name="aAnimName">Case sensitive name of the animation</param>
        /// <returns>True if it's in the list, false, otherwise.</returns>
        public bool HasAnim     (string aAnimName)
        {
            return mAnimations.ContainsKey(aAnimName);
        }

        /// <summary>
        /// Converts a tile ID into a rectangle on the spritesheet.
        /// </summary>
        /// <param name="aTileID">ID of the tile you're looking for.</param>
        /// <returns>A rectangle indicating a region on the spritesheet.</returns>
        protected Rectangle GetTileRect(int aTileID)
        {
            int x = aTileID % mGridWidth;
            int y = aTileID / mGridWidth;

            return new Rectangle(x * (CellSizeX),
                                 y * (CellSizeY),
                                 CellSizeX,
                                 CellSizeY);
        }

        #endregion

        #region Overrides

        public override void   LoadContent()
        {
            base.LoadContent();

            // figure the dimensions of the spritesheet
            mGridWidth  = mImage.Width  / CellSizeX;
            mGridHeight = mImage.Height / CellSizeY;
        }
        public override void   Update     (GameTime gameTime)
        {
            if (mCurrentAnim != "")
            {
                mAnimTime += (float)gameTime.ElapsedGameTime.TotalSeconds * AnimSpeed;
                Anim curr = mAnimations[mCurrentAnim];
                float frameTime = 0;
                bool draw = false;

                // if the animation time is over, how do we react?
                if (mAnimTime > curr.mDuration)
                {
                    // send the anim finished message
                    if (OnAnimFinish != null)
                        OnAnimFinish(this, mCurrentAnim);

                    if (curr.mNextAnim != "")
                        SetAnim(curr.mNextAnim);
                    else if (curr.mLoop)
                        mAnimTime -= curr.mDuration;
                }

                // update the source recangle
                for (int i = 0; i < curr.mAnim.Length; i++)
                {
                    frameTime += curr.mAnim[i].mDuration;

                    if (mAnimTime < frameTime)
                    {
                        Source = GetTileRect(curr.mAnim[i].mTile);
                        draw = true;
                        break;
                    }
                }
                if (!draw)
                    Source = GetTileRect(curr.mAnim[curr.mAnim.Length - 1].mTile);
            }
            base.Update(gameTime);
        }
        public override object Clone      ()
        {
            ComActor2D com = (ComActor2D )base.Clone();
            com.mAnimations  = mAnimations;
            com.SetAnim( mCurrentAnim );
            return com;
        }
        #endregion
    }
}
